use super::*;
use indicatif::{HumanDuration, ProgressBar, ProgressStyle};
// use std::borrow::BorrowMut;
use std::cell::RefCell;
use std::fmt::{Display, Formatter};
use std::rc::Rc;
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::time::Instant;

use console::{Style, Term};

// static CMD_WM_SIZE: ShellCmd<String + 'static> = ShellCmd::new(&["wm", "size"], None);

const SIZE: &str = "size";
const DENSITY: &str = "density";
const SN: &str = "sn";
const KEYBOARD: &str = "keyboard";

// adb shell + DEVICE_ITEMS.1
// (collect task name, adb shell args)
const DEVICE_ITEMS: &[(&str, &str)] = &[
  (SIZE, "wm size"),
  (DENSITY, "wm density"),
  (SN, "getprop ro.serialno"),
  (KEYBOARD, "settings get secure default_input_method"),
];

// touple(width,height)
//
#[derive(Debug, Clone)]
pub enum DeviceSpecifications {
  ScreenDensity(u32),
  ScreenSize(u32, u32),
  SerialNum(String),
  Keyboard(String),
}

impl Display for DeviceSpecifications {
  fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
    match self {
      DeviceSpecifications::ScreenDensity(d) => write!(f, "density={}", d),
      DeviceSpecifications::ScreenSize(w, h) => write!(f, "(width,height)=({},{})", w, h),
      DeviceSpecifications::SerialNum(s) => write!(f, "sn={}", s),
      DeviceSpecifications::Keyboard(s) => write!(f, "keyboard={}", s),
    }
  }
}

pub struct CollectCmd;

impl CollectCmd {
  pub fn new() -> Self {
    CollectCmd
  }
}

impl BinaryCommand for CollectCmd {
  type Output = Vec<Option<DeviceSpecifications>>;
  fn exec(&self) -> Self::Output {
    if cfg!(target_os = "windows") {
      panic!("no impl for windows!")
    } else {
      let mut output: Vec<Option<DeviceSpecifications>> = Vec::with_capacity(DEVICE_ITEMS.len());
      const NUM_CPUS: usize = 4;
      let start = Instant::now();
      lazy_static! {
        static ref SIZE_RE: Regex = Regex::new(r"Physical size:[^\d]*(\d+)x(\d+)[^\d]*").unwrap();
        static ref DENSITY_RE: Regex = Regex::new(r"Physical density:[^\d]*(\d+)[^\d]*").unwrap();
      }

      // mimic cargo progress bar although it behaves a bit different
      let pb = ProgressBar::new(DEVICE_ITEMS.len() as u64);
      pb.set_style(
        ProgressStyle::with_template(
          // note that bar size is fixed unlike cargo which is dynamic
          // and also the truncation in cargo uses trailers (`...`)
          if Term::stdout().size().1 > 80 {
            "{prefix:>12.cyan.bold} [{bar:57}] {pos}/{len} {wide_msg}"
          } else {
            "{prefix:>12.cyan.bold} [{bar:57}] {pos}/{len}"
          },
        )
        .unwrap()
        .progress_chars("=> "),
      );
      pb.set_prefix("Collecting");

      // process in another thread
      // items to be iterated but not exactly a tree
      let items = Arc::new(Mutex::new(DEVICE_ITEMS.iter()));
      let (tx, rx) = mpsc::channel();
      for n in 0..NUM_CPUS {
        let tx = tx.clone();
        let items = items.clone();

        thread::spawn(move || {
          loop {
            let item = items.lock().unwrap().next();
            let specification: Rc<RefCell<Option<DeviceSpecifications>>> =
              Rc::new(RefCell::new(None));
            // notify main thread if n thread is processing a item
            if let Some(item) = item {
              // let catched = Rc::new(RefCell::new(String::from("?")));
              // let catched1 = Rc::clone(&catched);
              let specification1 = specification.clone();
              let closure: Box<dyn Fn(&str) -> String> = match item {
                (DENSITY, _) => Box::new(move |s: &str| {
                  for cap in DENSITY_RE.captures_iter(&s) {
                    // *catched1.borrow_mut() = cap[1].trim().to_string();
                    // self.borrow_mut().parse_density(cap[1].to_string());
                    // println!("density result=>{}", cap[1].to_string());
                    let d = cap[1]
                      .to_string()
                      .parse::<u32>()
                      .expect("parse density_str fail!");

                    *specification1.borrow_mut() = Some(DeviceSpecifications::ScreenDensity(d));
                    break;
                  }
                  String::from("")
                }),
                (SIZE, _) => Box::new(move |s: &str| {
                  for cap in SIZE_RE.captures_iter(&s) {
                    // *catched1.borrow_mut() = cap[1].trim().to_string();
                    // self.borrow_mut().parse_size(cap[1].to_string(), cap[2].to_string());
                    // println!("size result=>{},{}", cap[1].to_string(), cap[2].to_string());
                    let w = cap[1]
                      .to_string()
                      .parse::<u32>()
                      .expect("parse width_str fail!");
                    let h = cap[2]
                      .to_string()
                      .parse::<u32>()
                      .expect("parse height_str fail!");

                    *specification1.borrow_mut() = Some(DeviceSpecifications::ScreenSize(w, h));
                    break;
                  }
                  String::from("")
                }),
                (SN, _) => Box::new(move |s: &str| {
                  *specification1.borrow_mut() =
                    Some(DeviceSpecifications::SerialNum(s.trim().to_string()));
                  String::from("")
                }),
                (KEYBOARD, _) => Box::new(move |s: &str| {
                  *specification1.borrow_mut() =
                    Some(DeviceSpecifications::Keyboard(s.trim().to_string()));
                  String::from("")
                }),
                _ => Box::new(move |_: &str| String::from("")),
              };

              ShellCmd::new(&[item.1], Some(closure))
                .exec()
                .expect("collect cmd fail");
            } else {
              break;
            }
            tx.send((n, item, specification.borrow_mut().take()))
              .unwrap();
          }
        });
      }
      // drop tx to stop waiting
      drop(tx);

      let green_bold = Style::new().green().bold();
      let red_bold = Style::new().red().bold();

      // do progress drawing in main thread
      let mut processing = vec![None; NUM_CPUS];
      while let Ok((n, item, specification)) = rx.recv() {
        processing[n] = item;
        let items: Vec<&str> = processing
          .iter()
          .filter_map(|t| t.copied().map(|(name, _)| name))
          .collect();
        pb.set_message(items.join(", "));
        if let Some((name, _)) = item {
          // item is being built
          let line = format!(
            "{:>12} {} => {}",
            iif!(
              specification.is_some(),
              green_bold.apply_to("Collected"),
              red_bold.apply_to("Failed")
            ),
            name,
            specification
              .as_ref()
              .map_or("".to_string(), |v| v.to_string())
          );
          pb.println(line);

          pb.inc(1);
        }
        output.push(specification);
      }
      pb.finish_and_clear();

      // compilation is finished
      println!(
        "{:>12} {} item(s) in {}",
        green_bold.apply_to("Finished"),
        DEVICE_ITEMS.len(),
        HumanDuration(start.elapsed())
      );

      output
    }
  }
}
